// SPDX-License-Identifier: GPL-2.0-or-later
/* Driver for Virtio QTBox device.
 *
 * Copyright 2022 HUAWEI TECHNOLOGIES CO., LTD.
 */

#include <linux/err.h>
#include <linux/slab.h>
#include <linux/delay.h>
#include "uapi/virtio_qtbox.h"
#include "virtio_qtbox_comm.h"

#define DEFAULT_CMD_TIMEOUT_MS	(30000) /* 30s */

int virtio_qtbox_do_cmd(struct virtio_qtbox *qtbox, uint32_t opcode, uint32_t subcode,
			void *para_flf, size_t flf_len, void *para_vlf, size_t vlf_len,
			void *result, size_t result_len)
{
	const unsigned long timeout = DEFAULT_CMD_TIMEOUT_MS + jiffies;
	struct virtio_qtbox_cmd_header *header;
	void *out_flf, *out_vlf = NULL;
	void *in_result;
	uint32_t *in_status;
	struct scatterlist sg_header, sg_flf, sg_vlf;
	struct scatterlist sg_status, sg_result;
	struct scatterlist *sgs[5] = { NULL };
	unsigned int tmp, num_out, num_in;
	int ret;

	ret = -ENOMEM;
	header = kzalloc(sizeof(*header), GFP_KERNEL);
	if (!header)
		goto err_alloc_header;

	out_flf = kmemdup(para_flf, flf_len, GFP_ATOMIC);
	if (!out_flf)
		goto err_alloc_flf;

	if (para_vlf && vlf_len) {
		out_vlf = kmemdup(para_vlf, vlf_len, GFP_ATOMIC);
		if (!out_vlf)
			goto err_alloc_vlf;
	}

	in_status = kzalloc(sizeof(*in_status), GFP_KERNEL);
	if (!in_status)
		goto err_alloc_status;

	in_result = kzalloc(result_len, GFP_KERNEL);
	if (!in_result)
		goto err_alloc_result;

	header->op_code = cpu_to_le32(opcode);
	header->subcode = cpu_to_le32(subcode);
	num_out = 0;
	num_in = 0;

	sg_init_one(&sg_header, header, sizeof(*header));
	sgs[num_out++] = &sg_header;
	sg_init_one(&sg_flf, out_flf, flf_len);
	sgs[num_out++] = &sg_flf;
	if (out_vlf) {
		sg_init_one(&sg_vlf, out_vlf, vlf_len);
		sgs[num_out++] = &sg_vlf;
	}
	sg_init_one(&sg_status, in_status, sizeof(*in_status));
	sgs[num_out + num_in++] = &sg_status;
	sg_init_one(&sg_result, in_result, result_len);
	sgs[num_out + num_in++] = &sg_result;

	mutex_lock(&qtbox->cmdq_mutex);

	ret = virtqueue_add_sgs(qtbox->cmdq, sgs, num_out,
				num_in, qtbox, GFP_ATOMIC);
	if (ret < 0)
		goto err_add_sgs;
	virtqueue_kick(qtbox->cmdq);

	while (!virtqueue_get_buf(qtbox->cmdq, &tmp) &&
	       !virtqueue_is_broken(qtbox->cmdq)) {
		/* sleep 1ms - 1.1ms */
		usleep_range(1000, 1100);
		if (time_after(jiffies, timeout)) {
			/*
			 * Note:
			 *  This indicates something wrong in the qtbox
			 *  device, maybe it would DMA to the memory after
			 *  we release the buffers later.
			 */
			ret = -ETIMEDOUT;
			goto err_add_sgs;
		}
	}

	ret = (int)le32_to_cpu(*in_status);
	memcpy(result, in_result, result_len);

err_add_sgs:
	mutex_unlock(&qtbox->cmdq_mutex);
	kfree(in_result);
err_alloc_result:
	kfree(in_status);
err_alloc_status:
	/* nothing to do if out_vlf == NULL */
	kfree(out_vlf);
err_alloc_vlf:
	kfree(out_flf);
err_alloc_flf:
	kfree(header);
err_alloc_header:
	return ret;
}

